#
# This is meant to be a really-fast, as-simple-as-possible tracker.
#

__author__ = 'gavinandresen@gmail.com (Gavin Andresen)'

import cgi
import google.appengine.api.memcache as memcache
import logging
import os
import random
from struct import pack

from BTL.bencode import bencode

def response(bencoded_dict):
    print "Content-type: text/plain"
    print ""
    print bencode(bencoded_dict), # Make sure we don't add a trailing new line!

def error(string):
    response({'failure reason': string})

# convert (ip, port) to 6-byte compact string:
def compact_peer(ip, port):
    i = ip.split('.')
    if len(i) != 4:
        return ''
    return pack('>4BH', int(i[0]), int(i[1]), int(i[2]), int(i[3]), port)
def compact_peers(peers, skip):
    result = ''
    for peer in peers:
        if peer == skip: continue
        result += compact_peer(peer[0], peer[1])
    return result

def find_peer(peers, peer):
    n = len(peers)/6
    for i in xrange(0,n):
        if peers[i*6:(i+1)*6] == peer:
            return i
    return -1

def main():
    """
    Implement the bittorrent tracker HTTP protocol.
    See http://www.bittorrent.org/beps/bep_0003.html for specs.
    """
    INTERVAL = 60*15 # Ask peers to check in every 15 minutes

    params = cgi.parse_qs(os.environ['QUERY_STRING'])
    if 'info_hash' not in params:
        error("Missing info_hash argument")
        return
    info_hash = params['info_hash'][0]
    if len(info_hash) != 20:
        error("Invalid info_hash argument (%s length %d != 20)" % (info_hash, len(info_hash)))
        return
    if 'ip' in params:
        ip = params['ip'][0]
    else:
        ip = os.environ['REMOTE_ADDR']
    port = int(params['port'][0])

    max_peers = 30
    if 'numwant' in params:
        max_peers = min(params['numwant'][0], 30)

    # event is optional: started/completed/stopped
    if 'event' not in params:
        params['event'] = ['']

    this_peer_string = compact_peer(ip, port)
    peer_string = memcache.get(info_hash)
    if peer_string is None:
        peer_string = ''

    if params['event'][0] == 'stopped':
        memcache.delete(this_peer_string)
        which_peer = find_peer(peer_string, this_peer_string)
        if which_peer != -1:
            peer_string = peer_string[:which_peer*6] + peer_string[(which_peer+1)*6:]
            memcache.set(info_hash, peer_string)
        response({'peers':[]}) # Empty, but valid, response.
        return

    # Any event besides stopped: remember peer checked in:
    memcache.set(this_peer_string, value=info_hash, time=INTERVAL*2)
    
    # If we don't already know about this peer, add it.
    # NOTE: writes to [info_hash]=peers aren't synchronized, so there is a
    # race condition here; peers will be lost (not end up on the list) if
    # more than one peer is 'started' at the same time.  That's not terrible,
    # because:
    #  1. it should be a rare occurrence
    #  2. it self-corrects after INTERVAL seconds when the lost peer checks in again
    #  3. the only bad effect is the lost peer won't be given out
    which_peer = find_peer(peer_string, this_peer_string)
    if which_peer == -1:
        peer_string += this_peer_string
        memcache.set(info_hash, peer_string)

    if max_peers <= 0:   # Seeders will set numwant to zero:
        response({'interval': INTERVAL, 'peers':[]})
        return

    # If peers go away without sending a 'stopped' message the memcache[info_hash]=peers string
    # can fill up with garbage, non-existent peers.  We know they're garbage if their entry
    # in memcache[peer_string] has expired (if they haven't checked in in INTERVAL*2 seconds).
    # So, garbage collect on 'started' events before returning a sample of peers:
    if params['event'][0] == 'started':
        n = len(peer_string)/6
        peer_list = [ peer_string[i*6:(i+1)*6] for i in xrange(0,n) ]
        alive_peer_list = [ p for p in peer_list if memcache.get(p) is not None ]
        peer_string = ''.join(alive_peer_list)
        memcache.set(info_hash, peer_string)

    n = len(peer_string)/6
    if n < max_peers:
        response({'interval': INTERVAL, 'peers': peer_string})
        return
        
    # Pick max_peers out of the n peer string:
    p = ''
    for i in random.sample(xrange(0,n), max_peers):
        p += peer_string[i*6:(i+1)*6]

    response({'interval': INTERVAL, 'peers': p})

        
if __name__ == "__main__":
    main()
